Skip to content

perf(lsp): resolve code lenses eagerly to fix ~25s document-open stall - #13

Merged
klaidliadon merged 10 commits into
masterfrom
perf/codelens-eager-parse
Jun 20, 2026
Merged

klaidliadon merged 10 commits into
masterfrom
perf/codelens-eager-parse

Conversation

@klaidliadon

Copy link
Copy Markdown
Collaborator

Opening a RIDL document fired ~17 codeLens/resolve calls (one per enum/struct/error), and each one re-parsed the entire workspace — and every parse recursively re-parsed the whole import graph twice (the schema build plus a second upstream pass). On a real project that was ~1.5s per resolve, ~25s of CPU per document open, serialized behind the editor. This makes CodeLens compute its reference counts eagerly in one pass and parse each file once per request via an AST-only parser, so a document open is a single workspace traversal instead of 17 full re-scans.


What changed

  • Eager CodeLens. CodeLens now returns fully-resolved lenses (reference-count Command already set); CodeLensResolve is a passthrough and ResolveProvider is advertised as false. This removes the per-lens resolve storm and the staleness surface of a per-version resolve memo — the lens list is a snapshot of one CodeLens request, which the client re-issues on change.
  • ParseAST (new). An overlay-aware, AST-only parse: the upstream AST head only, skipping buildPartialSchema's recursive import parse and the second ridl.NewParser schema pass. Root is structurally identical to a full Parse; Schema is a non-nil empty value; a partial root is still returned on parse error (mid-edit keeps working).
  • parseFn injection. Definition/reference resolution is parametrized by a parse function. The Server methods stay as adapters binding the existing full-parse path, so every non-CodeLens handler is unchanged; only CodeLens injects a request-scoped memoized AST parser (keyed by filepath.Clean, overlays snapshotted once, open-buffer results reused).
  • Cancellation. The eager scan honors request cancellation: a cancelled request returns ctx.Err() and never leaks a partial or empty result as success.

This is PR1 of two. PR2 (separate) adds a session-level parse cache + **/*.ridl file watchers to speed references/rename/diagnostics across edits; it is intentionally out of scope here.

Test plan

$ make test
ok  github.com/webrpc/ridl-lsp/cmd/ridl-lsp
ok  github.com/webrpc/ridl-lsp/internal/documents
ok  github.com/webrpc/ridl-lsp/internal/lsp
ok  github.com/webrpc/ridl-lsp/internal/ridl
(146 tests pass)

$ go test -race ./internal/lsp/ ./internal/ridl/
ok  github.com/webrpc/ridl-lsp/internal/lsp
ok  github.com/webrpc/ridl-lsp/internal/ridl
(no races)

$ make lint
0 issues.

$ go test ./internal/lsp/ -bench BenchmarkCodeLensEager -run x -benchtime 5x
BenchmarkCodeLensEager-14  ~3.9 ms/op   (40-importer workspace; was ~17 × full-workspace re-parse)

New tests: ParseAST returns Root + non-nil Schema and skips import resolution; ParseAST vs Parse Root structural equivalence; request-scoped memo parses each path once and is keyed by cleaned path; eager CodeLens builds the show-references command with correct counts for same-file, imported-type, and error references; CodeLensResolve passthrough; cancelled-context CodeLens returns an error.

Review notes

  • Design converged with Codex up front (root-cause + staging + the ParseAST/eager-CodeLens shape), then the implementation diff went through a Claude per-task + whole-branch review (which caught the ResolveProvider/passthrough mismatch) and a Codex adversarial diff review (which caught the cancellation leak). Both are fixed.
  • Known non-blocking follow-ups: the benchmark inlines setupServer because it only takes *testing.T (make it testing.TB as cleanup); upstream parser execution itself is not interruptible mid-parse — cancellation is cooperative between file parses, which is sufficient here since the loop stops before each parse and before returning.

@klaidliadon
klaidliadon merged commit a77e055 into master Jun 20, 2026
1 check passed
@klaidliadon
klaidliadon deleted the perf/codelens-eager-parse branch June 20, 2026 22:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant